package com.tweetlanes.android.core.widget.pulltorefresh;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Interpolator;
import android.widget.LinearLayout;

import com.tweetlanes.android.core.R;
import com.tweetlanes.android.core.widget.pulltorefresh.internal.LoadingLayout;

public abstract class PullToRefreshBase<T extends View> extends LinearLayout {

    final class SmoothScrollRunnable implements Runnable {

        static final int ANIMATION_DURATION_MS = 190;
        static final int ANIMATION_FPS = 1000 / 60;

        private final Interpolator mInterpolator;
        private final int mScrollToY;
        private final int mScrollFromY;
        private final Handler mHandler;

        private boolean mContinueRunning = true;
        private long mStartTime = -1;
        private int mCurrentY = -1;

        public SmoothScrollRunnable(Handler handler, int fromY, int toY) {
            this.mHandler = handler;
            this.mScrollFromY = fromY;
            this.mScrollToY = toY;
            this.mInterpolator = new AccelerateDecelerateInterpolator();
        }

        @Override
        public void run() {

            /**
             * Only set startTime if this is the first time we're starting, else
             * actually calculate the Y delta
             */
            if (mStartTime == -1) {
                mStartTime = System.currentTimeMillis();
            } else {

                /**
                 * We do do all calculations in long to reduce software float
                 * calculations. We use 1000 as it gives us good accuracy and
                 * small rounding errors
                 */
                long normalizedTime = (1000 * (System.currentTimeMillis() - mStartTime))
                        / ANIMATION_DURATION_MS;
                normalizedTime = Math.max(Math.min(normalizedTime, 1000), 0);

                final int deltaY = Math.round((mScrollFromY - mScrollToY)
                        * mInterpolator
                        .getInterpolation(normalizedTime / 1000f));
                this.mCurrentY = mScrollFromY - deltaY;
                setHeaderScroll(mCurrentY);
            }

            // If we're not at the target Y, keep going...
            if (mContinueRunning && mScrollToY != mCurrentY) {
                mHandler.postDelayed(this, ANIMATION_FPS);
            }
        }

        public void stop() {
            this.mContinueRunning = false;
            this.mHandler.removeCallbacks(this);
        }
    }

    // ===========================================================
    // Constants
    // ===========================================================

    private static final float FRICTION = 2.0f;

    private static final int PULL_TO_REFRESH = 0x0;
    private static final int RELEASE_TO_REFRESH = 0x1;
    private static final int REFRESHING = 0x2;
    private static final int MANUAL_REFRESHING = 0x3;

    public static final int MODE_PULL_DOWN_TO_REFRESH = 0x1;
    public static final int MODE_PULL_UP_TO_REFRESH = 0x2;
    static final int MODE_BOTH = 0x3;

    // ===========================================================
    // Fields
    // ===========================================================

    private int mTouchSlop;

    private float mInitialMotionY;
    private float mLastMotionX;
    private float mLastMotionY;
    private boolean mIsBeingDragged = false;

    private int mState = PULL_TO_REFRESH;
    private int mMode = MODE_PULL_DOWN_TO_REFRESH;
    private int mCurrentMode;

    private boolean mDisableScrollingWhileRefreshing = true;

    T mRefreshableView;
    private boolean mIsPullToRefreshEnabled = true;

    private LoadingLayout mHeaderLayout;
    private LoadingLayout mFooterLayout;
    private int mHeaderHeight;

    private final Handler mHandler = new Handler();

    private OnRefreshListener mOnRefreshListener;

    private SmoothScrollRunnable mCurrentSmoothScrollRunnable;

    // ===========================================================
    // Constructors
    // ===========================================================

    PullToRefreshBase(Context context) {
        super(context);
        init(context, null);
    }

    PullToRefreshBase(Context context, int mode) {
        super(context);
        this.mMode = mode;
        init(context, null);
    }

    PullToRefreshBase(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }

    // ===========================================================
    // Getter & Setter
    // ===========================================================

    /**
     * Deprecated. Use {@link #getRefreshableView()} from now on.
     *
     * @return The Refreshable View which is currently wrapped
     * @deprecated
     */
    public final T getAdapterView() {
        return mRefreshableView;
    }

    /**
     * Get the Wrapped Refreshable View. Anything returned here has already been
     * added to the content view.
     *
     * @return The View which is currently wrapped
     */
    public final T getRefreshableView() {
        return mRefreshableView;
    }

    /**
     * Whether Pull-to-Refresh is enabled
     *
     * @return enabled
     */
    public final boolean isPullToRefreshEnabled() {
        return mIsPullToRefreshEnabled;
    }

    /**
     * Returns whether the widget has disabled scrolling on the Refreshable View
     * while refreshing.
     *
     * @param true if the widget has disabled scrolling while refreshing
     */
    public final boolean isDisableScrollingWhileRefreshing() {
        return mDisableScrollingWhileRefreshing;
    }

    /**
     * Returns whether the Widget is currently in the Refreshing state
     *
     * @return true if the Widget is currently refreshing
     */
    final boolean isRefreshing() {
        return mState == REFRESHING || mState == MANUAL_REFRESHING;
    }

    /**
     * By default the Widget disabled scrolling on the Refreshable View while
     * refreshing. This method can change this behaviour.
     *
     * @param disableScrollingWhileRefreshing - true if you want to disable scrolling while refreshing
     */
    final void setDisableScrollingWhileRefreshing(
            boolean disableScrollingWhileRefreshing) {
        this.mDisableScrollingWhileRefreshing = disableScrollingWhileRefreshing;
    }

    /**
     * Mark the current Refresh as complete. Will Reset the UI and hide the
     * Refreshing View
     */
    public final void onRefreshComplete() {
        if (mState != PULL_TO_REFRESH) {
            resetHeader();
        }
    }

    /**
     * Set OnRefreshListener for the Widget
     *
     * @param listener - Listener to be used when the Widget is set to Refresh
     */
    public final void setOnRefreshListener(OnRefreshListener listener) {
        mOnRefreshListener = listener;
    }

    /**
     * A mutator to enable/disable Pull-to-Refresh for the current View
     *
     * @param enable Whether Pull-To-Refresh should be used
     */
    public final void setPullToRefreshEnabled(boolean enable) {
        this.mIsPullToRefreshEnabled = enable;
    }

    /**
     * Set Text to show when the Widget is being pulled, and will refresh when
     * released
     *
     * @param releaseLabel - String to display
     */
    void setReleaseLabel(String releaseLabel) {
        if (null != mHeaderLayout) {
            mHeaderLayout.setReleaseLabel(releaseLabel);
        }
        if (null != mFooterLayout) {
            mFooterLayout.setReleaseLabel(releaseLabel);
        }
    }

    /**
     * Set Text to show when the Widget is being Pulled
     *
     * @param pullLabel - String to display
     */
    void setPullLabel(String pullLabel) {
        if (null != mHeaderLayout) {
            mHeaderLayout.setPullLabel(pullLabel);
        }
        if (null != mFooterLayout) {
            mFooterLayout.setPullLabel(pullLabel);
        }
    }

    /**
     * Set Text to show when the Widget is refreshing
     *
     * @param refreshingLabel - String to display
     */
    void setRefreshingLabel(String refreshingLabel) {
        if (null != mHeaderLayout) {
            mHeaderLayout.setRefreshingLabel(refreshingLabel);
        }
        if (null != mFooterLayout) {
            mFooterLayout.setRefreshingLabel(refreshingLabel);
        }
    }

    final void setRefreshing() {
        this.setRefreshing(true);
    }

    /**
     * Sets the Widget to be in the refresh state. The UI will be updated to
     * show the 'Refreshing' view.
     *
     * @param doScroll - true if you want to force a scroll to the Refreshing view.
     */
    public final void setRefreshing(boolean doScroll) {
        if (!isRefreshing()) {
            setRefreshingInternal(doScroll);
            mState = MANUAL_REFRESHING;
        }
    }

    public final boolean hasPullFromTop() {
        return mCurrentMode != MODE_PULL_UP_TO_REFRESH;
    }

    public final boolean showingRefreshUI() {
        return mCurrentMode != 0;
    }

    // ===========================================================
    // Methods for/from SuperClass/Interfaces
    // ===========================================================

    @Override
    public final boolean onTouchEvent(MotionEvent event) {
        if (!mIsPullToRefreshEnabled) {
            return false;
        }

        if (isRefreshing() && mDisableScrollingWhileRefreshing) {
            return true;
        }

        if (event.getAction() == MotionEvent.ACTION_DOWN
                && event.getEdgeFlags() != 0) {
            return false;
        }

        switch (event.getAction()) {

            case MotionEvent.ACTION_MOVE: {
                if (mIsBeingDragged) {
                    mLastMotionY = event.getY();
                    this.pullEvent();
                    return true;
                }
                break;
            }

            case MotionEvent.ACTION_DOWN: {
                if (isReadyForPull()) {
                    mLastMotionY = mInitialMotionY = event.getY();
                    return true;
                }
                break;
            }

            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP: {
                if (mIsBeingDragged) {
                    mIsBeingDragged = false;

                    if (mState == RELEASE_TO_REFRESH && null != mOnRefreshListener) {
                        setRefreshingInternal(true);
                        mOnRefreshListener.onRefresh();
                    } else {
                        smoothScrollTo(0);
                    }
                    return true;
                }
                break;
            }
        }

        return false;
    }

    @Override
    public final boolean onInterceptTouchEvent(MotionEvent event) {

        if (!mIsPullToRefreshEnabled) {
            return false;
        }

        if (isRefreshing() && mDisableScrollingWhileRefreshing) {
            return true;
        }

        final int action = event.getAction();

        if (action == MotionEvent.ACTION_CANCEL
                || action == MotionEvent.ACTION_UP) {
            mIsBeingDragged = false;
            return false;
        }

        if (action != MotionEvent.ACTION_DOWN && mIsBeingDragged) {
            return true;
        }

        switch (action) {
            case MotionEvent.ACTION_MOVE: {
                if (isReadyForPull()) {

                    final float y = event.getY();
                    final float dy = y - mLastMotionY;
                    final float yDiff = Math.abs(dy);
                    final float xDiff = Math.abs(event.getX() - mLastMotionX);

                    if (yDiff > mTouchSlop && yDiff > xDiff) {
                        if ((mMode == MODE_PULL_DOWN_TO_REFRESH || mMode == MODE_BOTH)
                                && dy >= 0.0001f && isReadyForPullDown()) {
                            mLastMotionY = y;
                            mIsBeingDragged = true;
                            if (mMode == MODE_BOTH) {
                                mCurrentMode = MODE_PULL_DOWN_TO_REFRESH;
                            }
                        } else if ((mMode == MODE_PULL_UP_TO_REFRESH || mMode == MODE_BOTH)
                                && dy <= 0.0001f && isReadyForPullUp()) {
                            mLastMotionY = y;
                            mIsBeingDragged = true;
                            if (mMode == MODE_BOTH) {
                                mCurrentMode = MODE_PULL_UP_TO_REFRESH;
                            }
                        }
                    }
                }
                break;
            }
            case MotionEvent.ACTION_DOWN: {
                if (isReadyForPull()) {
                    mLastMotionY = mInitialMotionY = event.getY();
                    mLastMotionX = event.getX();
                    mIsBeingDragged = false;
                }
                break;
            }
        }

        return mIsBeingDragged;
    }

    void addRefreshableView(Context context, T refreshableView) {
        addView(refreshableView, new LinearLayout.LayoutParams(
                LayoutParams.FILL_PARENT, 0, 1.0f));
    }

    /**
     * This is implemented by derived classes to return the created View. If you
     * need to use a custom View (such as a custom ListView), override this
     * method and return an instance of your custom class.
     * <p/>
     * Be sure to set the ID of the view in this method, especially if you're
     * using a ListActivity or ListFragment.
     *
     * @param context
     * @param attrs   AttributeSet from wrapped class. Means that anything you
     *                include in the XML layout declaration will be routed to the
     *                created View
     * @return New instance of the Refreshable View
     */
    protected abstract T createRefreshableView(Context context,
                                               AttributeSet attrs);

    final int getCurrentMode() {
        return mCurrentMode;
    }

    final LoadingLayout getFooterLayout() {
        return mFooterLayout;
    }

    final LoadingLayout getHeaderLayout() {
        return mHeaderLayout;
    }

    final int getHeaderHeight() {
        return mHeaderHeight;
    }

    final int getMode() {
        return mMode;
    }

    /**
     * Implemented by derived class to return whether the View is in a state
     * where the user can Pull to Refresh by scrolling down.
     *
     * @return true if the View is currently the correct state (for example, top
     * of a ListView)
     */
    protected abstract boolean isReadyForPullDown();

    /**
     * Implemented by derived class to return whether the View is in a state
     * where the user can Pull to Refresh by scrolling up.
     *
     * @return true if the View is currently in the correct state (for example,
     * bottom of a ListView)
     */
    protected abstract boolean isReadyForPullUp();

    // ===========================================================
    // Methods
    // ===========================================================

    void resetHeader() {
        mState = PULL_TO_REFRESH;
        mIsBeingDragged = false;

        if (null != mHeaderLayout) {
            mHeaderLayout.reset();
        }
        if (null != mFooterLayout) {
            mFooterLayout.reset();
        }

        smoothScrollTo(0);
    }

    void setRefreshingInternal(boolean doScroll) {
        mState = REFRESHING;

        if (null != mHeaderLayout) {
            mHeaderLayout.refreshing();
        }
        if (null != mFooterLayout) {
            mFooterLayout.refreshing();
        }

        if (doScroll) {
            smoothScrollTo(mCurrentMode == MODE_PULL_DOWN_TO_REFRESH ? -mHeaderHeight
                    : mHeaderHeight);
        }
    }

    final void setHeaderScroll(int y) {
        scrollTo(0, y);
    }

    final void smoothScrollTo(int y) {
        if (null != mCurrentSmoothScrollRunnable) {
            mCurrentSmoothScrollRunnable.stop();
        }

        if (this.getScrollY() != y) {
            this.mCurrentSmoothScrollRunnable = new SmoothScrollRunnable(
                    mHandler, getScrollY(), y);
            mHandler.post(mCurrentSmoothScrollRunnable);
        }
    }

    private void init(Context context, AttributeSet attrs) {

        setOrientation(LinearLayout.VERTICAL);

        mTouchSlop = ViewConfiguration.getTouchSlop();

        // Styleables from XML
        TypedArray a = context.obtainStyledAttributes(attrs,
                R.styleable.PullToRefresh);
        if (a.hasValue(R.styleable.PullToRefresh_mode)) {
            mMode = a.getInteger(R.styleable.PullToRefresh_mode,
                    MODE_PULL_DOWN_TO_REFRESH);
        }

        // Refreshable View
        // By passing the attrs, we can add ListView/GridView params via XML
        mRefreshableView = this.createRefreshableView(context, attrs);
        this.addRefreshableView(context, mRefreshableView);

        // Loading View Strings
        String pullLabel = context
                .getString(R.string.pull_to_refresh_pull_label);
        String refreshingLabel = context
                .getString(R.string.pull_to_refresh_refreshing_label);
        String releaseLabel = context
                .getString(R.string.pull_to_refresh_release_label);

        // Add Loading Views
        if (mMode == MODE_PULL_DOWN_TO_REFRESH || mMode == MODE_BOTH) {
            mHeaderLayout = new LoadingLayout(context,
                    MODE_PULL_DOWN_TO_REFRESH, releaseLabel, pullLabel,
                    refreshingLabel);
            addView(mHeaderLayout, 0, new LinearLayout.LayoutParams(
                    ViewGroup.LayoutParams.FILL_PARENT,
                    ViewGroup.LayoutParams.WRAP_CONTENT));
            measureView(mHeaderLayout);
            mHeaderHeight = mHeaderLayout.getMeasuredHeight();
        }
        if (mMode == MODE_PULL_UP_TO_REFRESH || mMode == MODE_BOTH) {
            mFooterLayout = new LoadingLayout(context, MODE_PULL_UP_TO_REFRESH,
                    releaseLabel, pullLabel, refreshingLabel);
            addView(mFooterLayout, new LinearLayout.LayoutParams(
                    ViewGroup.LayoutParams.FILL_PARENT,
                    ViewGroup.LayoutParams.WRAP_CONTENT));
            measureView(mFooterLayout);
            mHeaderHeight = mFooterLayout.getMeasuredHeight();
        }

        // Styleables from XML
        if (a.hasValue(R.styleable.PullToRefresh_headerTextColor)) {
            final int color = a.getColor(
                    R.styleable.PullToRefresh_headerTextColor, Color.BLACK);
            if (null != mHeaderLayout) {
                mHeaderLayout.setTextColor(color);
            }
            if (null != mFooterLayout) {
                mFooterLayout.setTextColor(color);
            }
        }
        if (a.hasValue(R.styleable.PullToRefresh_headerBackground)) {
            this.setBackgroundResource(a.getResourceId(
                    R.styleable.PullToRefresh_headerBackground, Color.WHITE));
        }
        if (a.hasValue(R.styleable.PullToRefresh_adapterViewBackground)) {
            mRefreshableView.setBackgroundResource(a.getResourceId(
                    R.styleable.PullToRefresh_adapterViewBackground,
                    Color.WHITE));
        }
        a.recycle();

        // Hide Loading Views
        switch (mMode) {
            case MODE_BOTH:
                setPadding(0, -mHeaderHeight, 0, -mHeaderHeight);
                break;
            case MODE_PULL_UP_TO_REFRESH:
                setPadding(0, 0, 0, -mHeaderHeight);
                break;
            case MODE_PULL_DOWN_TO_REFRESH:
            default:
                setPadding(0, -mHeaderHeight, 0, 0);
                break;
        }

        // If we're not using MODE_BOTH, then just set currentMode to current
        // mode
        if (mMode != MODE_BOTH) {
            mCurrentMode = mMode;
        }
    }

    private static void measureView(View child) {
        ViewGroup.LayoutParams p = child.getLayoutParams();
        if (p == null) {
            p = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT,
                    ViewGroup.LayoutParams.WRAP_CONTENT);
        }

        int childWidthSpec = ViewGroup.getChildMeasureSpec(0, 0, p.width);
        int lpHeight = p.height;
        int childHeightSpec;
        if (lpHeight > 0) {
            childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight,
                    MeasureSpec.EXACTLY);
        } else {
            childHeightSpec = MeasureSpec.makeMeasureSpec(0,
                    MeasureSpec.UNSPECIFIED);
        }
        child.measure(childWidthSpec, childHeightSpec);
    }

    /**
     * Actions a Pull Event
     *
     * @return true if the Event has been handled, false if there has been no
     * change
     */
    private boolean pullEvent() {

        final int newHeight;
        final int oldHeight = this.getScrollY();

        switch (mCurrentMode) {
            case MODE_PULL_UP_TO_REFRESH:
                newHeight = Math.round(Math.max(mInitialMotionY - mLastMotionY, 0)
                        / FRICTION);
                break;
            case MODE_PULL_DOWN_TO_REFRESH:
            default:
                newHeight = Math.round(Math.min(mInitialMotionY - mLastMotionY, 0)
                        / FRICTION);
                break;
        }

        setHeaderScroll(newHeight);

        if (newHeight != 0) {
            if (mState == PULL_TO_REFRESH
                    && mHeaderHeight < Math.abs(newHeight)) {
                mState = RELEASE_TO_REFRESH;

                switch (mCurrentMode) {
                    case MODE_PULL_UP_TO_REFRESH:
                        mFooterLayout.releaseToRefresh();
                        break;
                    case MODE_PULL_DOWN_TO_REFRESH:
                        mHeaderLayout.releaseToRefresh();
                        break;
                }

                return true;

            } else if (mState == RELEASE_TO_REFRESH
                    && mHeaderHeight >= Math.abs(newHeight)) {
                mState = PULL_TO_REFRESH;

                switch (mCurrentMode) {
                    case MODE_PULL_UP_TO_REFRESH:
                        mFooterLayout.pullToRefresh();
                        break;
                    case MODE_PULL_DOWN_TO_REFRESH:
                        mHeaderLayout.pullToRefresh();
                        break;
                }

                return true;
            }
        }

        return oldHeight != newHeight;
    }

    private boolean isReadyForPull() {
        switch (mMode) {
            case MODE_PULL_DOWN_TO_REFRESH:
                return isReadyForPullDown();
            case MODE_PULL_UP_TO_REFRESH:
                return isReadyForPullUp();
            case MODE_BOTH:
                return isReadyForPullUp() || isReadyForPullDown();
        }
        return false;
    }

    // ===========================================================
    // Inner and Anonymous Classes
    // ===========================================================

    public static interface OnRefreshListener {

        public void onRefresh();

    }

    public static interface OnLastItemVisibleListener {

        public void onLastItemVisible();

    }

    @Override
    public void setLongClickable(boolean longClickable) {
        getRefreshableView().setLongClickable(longClickable);
    }
}
